﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using XmlServer.Listeners;
using XmlServer;
using XmlServer.ServiceContracts;
using XmlServer.ServiceImplementation;

namespace XmlServer.PoolServicer
{
    public class ClientHandler
    {
        #region Local Variables

        private TcpClient clientSocket;         // TCPSocket the client is "talking" on
        private NetworkStream networkStream;
        private string clientIPAddress;
        private int clientPort = 0;
        private byte[] bytes;                   // data buffer for incoming data
        System.Text.StringBuilder sbRequest;    // the request from the client

        // variable used to determine if this client handler is alive
        // if alive, the pool service will re-queue this object in the pool
        private bool continueProcess = false;

        private Listener listener;
        private int startTick = 0;
        private int stopTick = 0;

        #endregion

        #region Constructors

        public ClientHandler(TcpClient clientSocket, Listener listener)
        {
            startTick = System.Environment.TickCount;
            this.clientSocket = clientSocket;

            try
            {
                IPEndPoint ipe = (IPEndPoint)clientSocket.Client.RemoteEndPoint;
                this.clientIPAddress = ipe.Address.ToString();
                this.clientPort = ipe.Port;
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to get the client enrpoint/ipAddress", ex);
            }

            sbRequest = new StringBuilder();
            this.clientSocket.ReceiveTimeout = 50;
            networkStream = this.clientSocket.GetStream();
            bytes = new byte[this.clientSocket.ReceiveBufferSize];
            this.listener = listener;

            this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} client created and added to pool", ClientEndpoint()));

            continueProcess = true;
        }

        #endregion

        #region Public Methods

        public void Process()
        {
            // Notes:
            // This does not need to be in a loop
            // Reason: the pool services deQueues the request and calls this moethod.  It then
            // queues the request again.  When the request is re-processed again, more data is read (if any).
            //
            // Again, the process of DeQueueing and EnQueueing occurs until there is no more data to read, then the
            // ProcessDataRecieved() method is called.  This method will process the request and eventually call Close().
            //
            // The Close() method will will set the variable ContinueProcess to false (which is exposed through
            // the Alive property).  When Alive is false, the PoolServicer will not re-queue the request.

            try
            {
                this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Start reading requests from socket", ClientEndpoint()));

                int bytesRead = this.networkStream.Read(bytes, 0, (int)bytes.Length);

                this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Finished reading requests from socket", ClientEndpoint()));

                if (bytesRead > 0)
                {
                    // there might be more data, so store the data received so far.
                    sbRequest.Append(Encoding.ASCII.GetString(bytes, 0, bytesRead));
                }
                else
                {
                    // all of the data has been recevied
                    ProcessDataReceived();
                }
            }
            catch (IOException)
            {
                // all of the data has been recevied
                ProcessDataReceived();
            }
            catch (SocketException)
            {
                Close();
            }
        }

        public void Close()
        {
            try
            {
                this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Starting to close socket", ClientEndpoint()));

                this.continueProcess = false;
                networkStream.Close();
                clientSocket.Close();

                this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Finished closing socket", ClientEndpoint()));
            }
            catch { }
        }

        public string ClientEndpoint()
        {
            return String.Format("{0}:{1}", this.clientIPAddress, this.clientPort);
        }

        #endregion

        #region Properties

        public bool Alive
        {
            get { return this.continueProcess; }
            set { this.continueProcess = false; }
        }

        public string ClientIPAddress
        {
            get { return this.clientIPAddress; }            
        }

        public int ClientPort
        {
            get { return this.clientPort; }
        }

        public int StartTick
        {
            get { return this.startTick; }
        }

        public int StopTick
        {
            get { return this.stopTick; }
        }

        public int ProcessingTime
        {
            get { return StopTick - StartTick; }
        }


        #endregion

        #region Private Methods


        /// <summary>
        /// This is an important method....It performs the following steps
        /// 1. Convert the request string from the client into an XmlDocument
        /// 2. Gets the name of the Operator that needs to be executed
        /// 3. Checks to see if the request needs to be relayed to another xmlServer
        /// 
        /// 99. Returns a response Xml string to the client
        /// </summary>
        private void ProcessDataReceived()
        {
            this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Starting to process client request", ClientEndpoint()));

            string replyData = String.Empty;

            if (sbRequest.Length > 0)
            {
                var request = sbRequest.ToString();
                listener.InvokeClientRequestServicingDelegate(this.clientIPAddress, this.ClientPort, request);

                // get the name of the operator to be invoked from the clientXML
                string operatorName = string.Empty;
                string version = string.Empty;
                string fullName = string.Empty;
                string relayIPaddress = string.Empty;
                int? relayPort = null;
                string errorMsg = string.Empty;

                // get the xml document of the request
                XmlDocument requestXml = XmlServer.Helper.Utilities.GetXmlDocument(request);                
                bool success = GetOperatorName(requestXml, out operatorName, out version, out fullName, out relayIPaddress, out relayPort, out errorMsg);

                if (success == false)
                {
                    // failed to get name of the operator to invoke
                    replyData = XmlServer.Helper.Utilities.CreateException(operatorName, version, "Failed to find the operator name in the request XML");
                }
                else
                {
                    if (!String.IsNullOrEmpty(relayIPaddress) && relayPort.HasValue)
                    {
                        // we have a request that should be relayed to another xml server
                        // remove the relay attributes from the request
                        XmlServer.Helper.Utilities.RemoveRelayAttributes(requestXml);

                        // send the request to the new xml server       
                        // .... We are actually the client now send the request to a new xmlServer...
                        // .......therefore, the request needs to be converted to a string
                        string newRequestXML = "";
                        using (var stringWriter = new StringWriter())
                        using (var xmlTextWriter = XmlWriter.Create(stringWriter))
                        {
                            requestXml.WriteTo(xmlTextWriter);
                            xmlTextWriter.Flush();
                            newRequestXML = stringWriter.GetStringBuilder().ToString();
                        }

                        replyData = XmlServer.Helper.Utilities.MakeRequest(newRequestXML, relayIPaddress, relayPort.Value);

                    }
                    else
                    {
                        if (fullName.Equals("status_1.0", StringComparison.InvariantCultureIgnoreCase))
                        {
                            // we have a status request
                            replyData = ProcessStatusRequest(request);
                        }
                        else
                        {
                            // we got a operator name; get the operator
                            this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Invoking processor to perform request", ClientEndpoint()));

                            OperatorManager.OperatorInvoker operatorInvoker = GetOperatorInvoker(fullName);
                            if (operatorInvoker == null)
                                replyData = XmlServer.Helper.Utilities.CreateException(fullName, version, errorMsg);
                            else
                            {
                                // invoke the Operator.  if this fails, the reply data will have the exception
                                InvokeOperator(operatorInvoker, requestXml, out replyData);
                            }
                            this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Finished invoking processor to perform request", ClientEndpoint()));
                        }
                    }
                }

                // send the response back to the client
                try
                {

                    this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Starting to write response to client", ClientEndpoint()));
                    byte[] sendBytes = Encoding.ASCII.GetBytes(replyData);

                    var sendBytesLength = sendBytes.Length;
                    var tcpSendBufferSize = clientSocket.SendBufferSize;
                    if (sendBytesLength <= tcpSendBufferSize)
                    {
                        // the reply is small enough to send 
                        networkStream.Write(sendBytes, 0, sendBytes.Length);
                    }
                    else
                    {
                        // the reply is too large....it needs to be sent in "chunks"
                        int offset = 0;
                        var chunkSize = tcpSendBufferSize;

                        while (offset < sendBytesLength)
                        {
                            networkStream.Write(sendBytes, offset, chunkSize);

                            offset += chunkSize;
                            if (offset == sendBytesLength) break;
                            if (offset + chunkSize > sendBytesLength) chunkSize = sendBytesLength - offset;

                        }

                    }
                        
                    this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Finished writing response to client", ClientEndpoint()));

                }
                catch
                {
                    // do nothing; the client may be disconnected
                }

            }

            this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Finished processing client request", ClientEndpoint()));
            this.Close();
            this.stopTick = System.Environment.TickCount;

            try
            {
                listener.InvokeClientRequestCompletedDelegate(this.ClientIPAddress, this.ClientPort, replyData, this.ProcessingTime);
            }
            catch { }

            this.listener.InvokeDebugMessageDelegate(String.Format("Stats: {0} => Client request finished", ClientEndpoint()));

        }

        private OperatorManager.OperatorInvoker GetOperatorInvoker(string operatorName)
        {
            var errorMsg = "";

            OperatorManager.OperatorInvoker operatorInvoker = null;
            try
            {
                operatorInvoker = listener.OperatorInvokers.GetOperatorInvoker(operatorName);
            }
            catch (Exception err)
            {
                errorMsg = String.Format("Failed to get the Operator '{0}'", operatorName);
                throw new Exception(errorMsg, err);                
            }

            if (operatorInvoker == null)
            {
                errorMsg = String.Format("Failed to find the Operator '{0}'", operatorName);
                throw new Exception(errorMsg);                
            }

            return operatorInvoker;
        }


        /// <summary>
        /// Extracts the operator name form the XML document
        /// </summary>
        private bool GetOperatorName(XmlDocument requestXml, out string operatorName, out string version, out string fullName, out string relayIPAddress, out int? relayPort, out string errorMsg)
        {

            operatorName = "";
            errorMsg = "";
            version = "";
            fullName = "";
            relayIPAddress = "";
            relayPort = null;

            XmlNode requestNode = null;
            try
            {
                requestNode = XmlServer.Helper.Utilities.GetRequestNode(requestXml);
                if (requestNode == null)
                {
                    errorMsg = "Error in ClientHandler.GetServerName(): Request node in XML is null or empty";
                    return false;
                }
            }
            catch (Exception err)
            {
                errorMsg = String.Format("Error in ClientHandler.GetServerName() while selecting the Request node in the XML: {0}", err.ToString());
                return false;
            }

            // loop through all the attribute variables and find the type, version
            try
            {
                for (int i = 0; i < requestNode.Attributes.Count; i++)
                {
                    XmlAttribute attribute = requestNode.Attributes[i];
                    switch (attribute.Name.Trim().ToLower())
                    {
                        case "version":
                            version = attribute.Value;
                            break;

                        case "type":
                            operatorName = attribute.Value;
                            break;

                        case "relayipaddress":
                            relayIPAddress = attribute.Value;
                            break;

                        case "relayport":
                            int intTemp = 0;
                            if (Int32.TryParse(attribute.Value, out intTemp)) relayPort = intTemp;
                            break;

                    }
                }
            }
            catch (Exception err)
            {
                errorMsg = String.Format("Error in ClientHandler.GetServerName() while getting the attributes 'Type' and 'Version' from the Request xml node: {0}", err.ToString());
                return false;
            }

            if (String.IsNullOrEmpty(operatorName) || String.IsNullOrEmpty(version))
            {
                errorMsg = "Error in ClientHandler.GetServerName(): Failed to get the 'Type' or 'Version' attribute from the request XML.  Invalid XML format";
                return false;
            }

            fullName = string.Format("{0}_{1}", operatorName, version);

            return true;

        }

        private void InvokeOperator(OperatorManager.OperatorInvoker operatorInvoker, XmlDocument request, out string response)
        {
            response = "";

            IResponse iResponse = null;

            bool success = true;
            string errorMsg = "";
            try
            {
                iResponse = operatorInvoker.PerformRequest(request);
            }
            catch (Exception err)
            {
                response = XmlServer.Helper.Utilities.CreateException(operatorInvoker.Name, operatorInvoker.Version, String.Format("Error in ClientHandler.InvokeOperator() while invoking the operator '{0}', Exception = {1}, Error Message = {2}", operatorInvoker.FullName, err.ToString(), errorMsg));
                return;
            }

            if (iResponse == null)
            {
                response = XmlServer.Helper.Utilities.CreateException(operatorInvoker.Name, operatorInvoker.Version, String.Format("Error in ClientHandler.InvokeOperator(): No results returned from operator '{0}'", operatorInvoker.FullName));
            }
            else
            {
                response = XmlServer.Helper.Utilities.CreateResponse(operatorInvoker.Name, operatorInvoker.Version, iResponse.Success, iResponse.Facts);
            }
            

        }


        #endregion

        #region Status Requests

        /// <summary>
        /// A client requested a status
        /// </summary>
        /// <param name="clientXML"></param>
        /// <returns></returns>
        private string ProcessStatusRequest(string request)
        {

            Listener priorityListener = null;
            try
            {
                priorityListener = this.listener.OtherListener;
            }
            catch (Exception err)
            {
                return "Error in ClientHandler.ProcessStatusRequest(): Failed to get the priorty listener: " + err.ToString();
            }

            return XmlServer.Helper.Utilities.CreateResponse("Status", "1.0", true,
                new List<IFact> 
                {
                    new Fact { Name = "Date", Value = DateTime.Now.ToUniversalTime().ToLongDateString() },
                    new Fact { Name = "Time", Value = DateTime.Now.ToUniversalTime().ToLongTimeString() },
                    new Fact { Name = "ProcessingThreadCount", Value = priorityListener.Configuration.NumberOfThreads.ToString() },
                    new Fact { Name = "WorkingThreadCount", Value = priorityListener.PoolServicer.WorkingThreads.ToString() },
                    new Fact { Name = "ClientsProcessed", Value = priorityListener.PoolServicer.ClientsProcessed.ToString() },
                    new Fact { Name = "ClientsTotalTime", Value = priorityListener.PoolServicer.TotalClientPoolTime.ToString() },
                    new Fact { Name = "ClientAverageTime", Value = priorityListener.PoolServicer.AverageClientPoolTime.ToString() },
                    new Fact { Name = "ClientPoolCount", Value = priorityListener.PoolServicer.ClientPoolCount.ToString() },
                    new Fact { Name = "EstimatedCleanupTime", Value = priorityListener.PoolServicer.EstimatedCleanupTime.ToString() }
                });

        }
            
        #endregion
    }
}
